package org.erp.common.aspect;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;

import org.erp.common.annotation.Idempotent;
import org.erp.common.exception.ServiceException;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.erp.common.redis.service.RedisService;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
/**
 * 幂等性处理
 *
 */
@Log4j2
@Aspect
@Order(1)
@Component
public class IdempotencyAspect {

    @Resource
    private RedisService redisService;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
        String key = buildIdempotencyKey(idempotent, pjp);
        if (key == null || key.isEmpty()) {
            // 无法构造幂等 key 时，直接执行
            return pjp.proceed();
        }
        try {
            Boolean ok = redisService.setIfAbsent(key, "1", idempotent.ttlSeconds(), TimeUnit.SECONDS);
            if (Boolean.FALSE.equals(ok)) {
                Long left = ttlLeftSeconds(key);
                if (left != null && left > 0) {
                    throw new ServiceException("重复请求，请稍后再试(" + left + "s)");
                }
                throw new ServiceException("重复请求，请稍后再试");
            }
            return pjp.proceed();
        } catch (ServiceException se) {
            throw se;
        } catch (Throwable t) {
            // 执行失败，允许立即重试：移除 key
            try { redisService.deleteObject(key); } catch (Exception ignore) {}
            throw t;
        }
    }

    private String buildIdempotencyKey(Idempotent idempotent, ProceedingJoinPoint pjp) {
        try {
            RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
            if (!(attrs instanceof ServletRequestAttributes)) {
                return null;
            }
            HttpServletRequest req = ((ServletRequestAttributes) attrs).getRequest();
            String uri = req.getRequestURI();
            String token = extractToken(req);
            String argsJson = safeToJson(pjp.getArgs());
            String tokenHash = sha256Base64(token == null ? "anonymous" : token);
            String payloadHash = sha256Base64(argsJson);
            return idempotent.prefix() + tokenHash + ":" + uri + ":" + payloadHash;
        } catch (Exception e) {
            log.warn("构造幂等key失败: {}", e.getMessage());
            return null;
        }
    }

    private String extractToken(HttpServletRequest req) {
        String auth = req.getHeader("Authorization");
        if (auth != null && !auth.isEmpty()) {
            // 兼容 Bearer 令牌
            if (auth.startsWith("Bearer ")) {
                return auth.substring(7);
            }
            return auth;
        }
        return null;
    }

    private String safeToJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            return String.valueOf(obj);
        }
    }

    private String sha256Base64(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
            return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("SHA-256 算法不可用", e);
        }
    }

    private Long ttlLeftSeconds(String key) {
        try {
            return redisService.getExpire(key, TimeUnit.SECONDS);
        } catch (Exception e) {
            return null;
        }
    }
}